/**
 * Middleware server
 */

package ResImpl;

import ResImpl.RMHashtable;
import ResImpl.RMItem;
import ResImpl.TransactionManager;
import ResInterface.*;
import LockManager.*;

import java.util.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Stack;
import java.io.ObjectOutputStream;
import java.rmi.*;

import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class MiddlewareServer implements ResourceManager {
	
	// For customers
	protected RMHashtable m_itemHT = new RMHashtable();

	private final static int MIDDLEWARE_PORT = 3501;
	private final static int FLIGHTS_PORT = 3502;
	private final static int CARS_PORT = 3503;
	private final static int HOTELS_PORT = 3504;
	
	private static LockManager mLockMan = new LockManager();

	// 0 = flights, 1 = cars, 2 = hotels for all data structures
	public final static int FLIGHTS = 0;
	public final static int CARS = 1;
	public final static int HOTELS = 2;

	private static Stack logger = new Stack();

	// The logging operation types
	private final static int COMMIT_COMMAND = 0;
	private final static int ABORT_COMMAND = 1;

	private static LinkedList<String> registry_names = new LinkedList<String>(Arrays.asList("rebdavFlightsRM", "rebdavCarsRM", "rebdavHotelsRM"));
	private static LinkedList<Registry> registries = new LinkedList<Registry>();
	private static LinkedList<ResourceManager> rms = new LinkedList<ResourceManager>();

	private static int[] ports = new int[] {FLIGHTS_PORT, CARS_PORT, HOTELS_PORT};
	
	// For my testing at home...
	private static String localServer = "142.157.8.133";
	private static String[] servers = new String[] {localServer, localServer, localServer};
	
	private static TransactionManager xManager = new TransactionManager();
	private boolean shutdown = false;

	/**
	 * @param args the list of active RMs
	 */
	public static void main(String[] args) {

			 if (args.length == 1) {
				 System.err.println("Include the names of active RMs as arguments.");
				 System.exit(1);
			 } else {
				 if(args.length != 3) {
					 System.err.println("Correct usage: MiddlewareServer flightsServer carsServer hotelsServer");
					 System.exit(1);
				 } else {
				for(int j = 0; j < 3; j++) {
					servers[j] = args[j];				
				}
			 }

				try 
				{
				// hard-coded to support three resources
					for(int i = 0; i < 3; i++) { 
						System.out.println("Trying to get registries now...");

						registries.add(i, LocateRegistry.getRegistry(servers[i], ports[i]));
						
						// get the proxy and the remote reference by rmiregistry lookup
						rms.add(i, (ResourceManager) registries.get(i).lookup(registry_names.get(i)));
						
						if(rms.get(i) != null)
						{
							System.out.println("Successful");
							System.out.println("Connected to " + registry_names.get(i));
			
						}
						else
						{
							System.out.println("Unsuccessful connection to " + registry_names.get(i));
						}
						}
					
				}
				catch (Exception e)
				{	
					System.err.println("Client exception: " + e.getMessage());
				}
			 }
		 
		 try
			 {
			 // now create a new Server object for self
			 MiddlewareServer obj = new MiddlewareServer();

			 // dynamically generate the stub (client proxy)
			 ResourceManager rm = (ResourceManager) UnicastRemoteObject.exportObject(obj, 0);
			 
			 // all clients can see the complete registry
			 Registry registry = LocateRegistry.getRegistry(MIDDLEWARE_PORT);
			 registry.rebind("rebdavMiddleware", rm);

			 System.err.println("Middleware ready");

			 } 

		 catch (Exception e) 
		 {
			 System.err.println("Server exception: " + e.getMessage());
		 }

		 try 
		 {
			 // hard-coded to support three resources
			 for(int i = 0; i < 3; i++) { 
				 System.out.println("Trying to get them to connect to me now...");

				 rms.get(i).connect();
				 System.out.println("COnnected.");
			 }

		 }
		 catch (Exception e)
		 {	
			 System.err.println("Client exception: " + e.getMessage());
		 }

	}


	public MiddlewareServer() throws RemoteException {

		System.out.println("Begin Middleware startup sequence...");
		File state = new File("mwCustomers");
		if(!state.isFile()){
			System.out.println("No previous customer records found");
		}
		else{
			System.out.println("Previous records found, recovering...");
			readCustomers();
		}
		// Deserialize the logger data structure
		readDiskAndResolve();
		

		 Thread t = new Thread(){
			 public void run(){
				 while(!shutdown){
					 try {
						Thread.sleep(5);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
					}
					LinkedList<Transaction> expired = xManager.getExpiredTransactions();
					for(int i = 0;i<expired.size();i++){
						try {
							System.out.println("Transaction: " + expired.get(i).getId() + " timed out");
							abort(expired.get(i).getId());
						} catch (RemoteException e) {
							// TODO Auto-generated catch block
							System.out.println(e.getMessage());
						} catch (InvalidTransactionException e) {
							// TODO Auto-generated catch block
						}
					}
				 }
			 }
		 };
		 t.start();
	 
	 }

	/* RM tells MW to rebind him */
	public void rebind(int resourceType) throws RemoteException {
		try {
			rms.add(resourceType, (ResourceManager) registries.get(resourceType).lookup(registry_names.get(resourceType)));
		} catch(Exception e) {	
			System.out.println("Could not rebind reference to that RM.");
		}
	}
	 
	// Read in the log and xManager after possible recovery.
	private void readDiskAndResolve() {
	 	try
		 {
	 		
	 		File log = new File("mw_log.ser");
	 		File xman = new File("mw_xman.ser");
	 		
			if(!log.isFile()){
				System.out.println("No previous log records found");
			}
			else{
				FileInputStream log_file = new FileInputStream(log);
			    ObjectInputStream in = new ObjectInputStream(log_file);
			    logger = (Stack) in.readObject();
			    in.close();
			    log_file.close();
			}

			if(!xman.isFile()){
				System.out.println("No previous manager records found");
			}
			else{
				FileInputStream x_file = new FileInputStream(xman);
				ObjectInputStream r = new ObjectInputStream(x_file);
				xManager = (TransactionManager) r.readObject();
				r.close();
				x_file.close();
			}

		 } catch(Exception i)
		 {
			 //i.printStackTrace();
			 return;
		 }

	 	int m;

	 	Log temp;


	 	for(int j = 0; j < logger.size(); j++) {
	 		// re-send all decisions in case they were not completed
	 		temp = (Log) logger.elementAt(j);
	 		//Ugly as sin, skips xactions that do not need to be dealt with
	 		if(xManager.getTransactionById(temp.getXID()).isAborted()||xManager.getTransactionById(temp.getXID()).isCommited()){
	 			continue;
	 		}
	 		if(temp.getResult() == COMMIT_COMMAND) {

	 			System.out.println("Sending recovery commits for transaction: " + temp.getXID());

	 			// Now if a RM dies then they will ask MW about the status of each transaction
	 			// it has on record.
	 			try {
	 				// All living ones voted YES; try to commit all. Only ask commits of alive RMs.
	 				if(rms.get(CARS).state()) {
	 					rms.get(CARS).commit(temp.getXID());
	 				}
	 			} catch (Exception e){
	 				System.out.println("Car server is down, he'll have to get back to me");
	 			}
	 			try{
	 				if(rms.get(FLIGHTS).state()) {
	 					rms.get(FLIGHTS).commit(temp.getXID());
	 				}
	 			} catch(Exception e){
	 				System.out.println("Flight server is down, he'll have to get back to me");
	 			}
	 			try{
	 				if(rms.get(HOTELS).state()) {
	 					rms.get(HOTELS).commit(temp.getXID());
	 				}
	 			} catch(Exception e){
	 				System.out.println("Hotel server is down, he'll have to get back to me");
	 			}
	 			xManager.commit(temp.getXID());
	 			temp.setResult(COMMIT_COMMAND);
	 			writeLog();
	 		} else {
	 			try {
					abort(temp.getXID());
				} catch (RemoteException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (InvalidTransactionException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} // ABORT does not send the vote-req so just call.
	 		}
	 	}
	 	
		// abort all unresolved transactions in the xManager
	 	LinkedList<Transaction> trans = xManager.getUnresolved();
	 	for(int i = 0; i < trans.size(); i++) {
	 		// abort all unresolved transactions
	 		m = trans.get(i).getId();
	 		try {
				abort(m);
			} catch (RemoteException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (InvalidTransactionException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
	 		logger.push(new Log(trans.get(i).getId(), ABORT_COMMAND));
	 	}
	 	writeLog();
	} 

	 // Re-writes the log to file. Called everytime the logger is updated.
	 private void writeLog() {
	 	ObjectOutputStream out;
		try {
			out = new ObjectOutputStream(new FileOutputStream("mw_log.ser"));
			out.writeObject(logger);
			out.flush();
			out.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			System.out.println("File not found");
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	 }
	
	// writes the transaction manager to disk.
	private void writeXMan() {
		 	ObjectOutputStream out;
			try {
				out = new ObjectOutputStream(new FileOutputStream("mw_xman.ser"));
				out.writeObject(xManager);
				out.flush();
				out.close();
			} catch (FileNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
	}
	
	/**
	 * Method will serialize the customers to disk
	 */
	private void writeCustomers(){
		File state = new File("mwCustomers");
		FileOutputStream fileOut;
		try {
			fileOut = new FileOutputStream(state);
			ObjectOutputStream objectWriter = new ObjectOutputStream(fileOut);
			objectWriter.writeObject(m_itemHT);
			objectWriter.flush();
			objectWriter.close();
			
		} catch (FileNotFoundException e) {
			System.out.println("Error finding file...why?");
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("General I/O error");
		}
	}
	/**
	 * Deserialize customers from disk
	 */
	private void readCustomers(){
		File state = new File("mwCustomers");
		FileInputStream fileIn;
		try {
			fileIn = new FileInputStream(state);
			ObjectInputStream objectReader = new ObjectInputStream(fileIn);
			m_itemHT = (RMHashtable) objectReader.readObject();
			objectReader.close();
			
		} catch (FileNotFoundException e) {
			System.out.println("Error finding file...why?");
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("General I/O error");
		} catch (ClassNotFoundException e) {
			System.out.println("Error finding the classes");
		}
	}

	 // Not implemented here; this just makes the interface happy.
	 public boolean connect() throws RemoteException {
		return false;
	 }

	 // Verify that the customer exists before sending remote requests
	 private boolean verifyCustomer(int id, int customerID) {
		Customer cust = (Customer) readData( id, Customer.getKey(customerID) );
		if( cust == null ) {
			Trace.warn("RM::verifyCustomer(" + id + ", " + customerID + ") failed--customer doesn't exist" );
			return false;
		} else {
			return true;
		} 
	 }

	// Reads a data item
	private RMItem readData( int id, String key )
	{
		synchronized(m_itemHT){
			
			return (RMItem) m_itemHT.get(key);
		}
	}

	// Writes a data item
	private void writeData( int id, String key, RMItem value )
	{
		synchronized(m_itemHT){
			
			m_itemHT.put(key, value);
		}
	}
	
	// Remove the item out of storage
	protected RMItem removeData(int id, String key){
		synchronized(m_itemHT){
			
			return (RMItem)m_itemHT.remove(key);
		}
	}
	
	
	// deletes the entire item
	protected boolean deleteItem(int id, String key)
	{ 
		Trace.info("RM::deleteItem(" + id + ", " + key + ") called" );
		ReservableItem curObj = (ReservableItem) readData( id, key );
		// Check if there is such an item in the storage
		if( curObj == null ) {
			Trace.warn("RM::deleteItem(" + id + ", " + key + ") failed--item doesn't exist" );
			return false;
		} else {
			if(curObj.getReserved()==0){
				removeData(id, curObj.getKey());
				Trace.info("RM::deleteItem(" + id + ", " + key + ") item deleted" );
				
				return true;
			}
			else{
				Trace.info("RM::deleteItem(" + id + ", " + key + ") item can't be deleted because some customers reserved it" );
				
				return false;
			}
		} // if
	}
	
	// Where is this used?
	// query the number of available seats/rooms/cars
	protected int queryNum(int id, String key) {
		Trace.info("RM::queryNum(" + id + ", " + key + ") called" );
		ReservableItem curObj = (ReservableItem) readData( id, key);
		int value = 0;  
		if( curObj != null ) {
			value = curObj.getCount();
		} // else
		Trace.info("RM::queryNum(" + id + ", " + key + ") returns count=" + value);
		
		return value;
	}	
	
	// query the price of an item
	protected int queryPrice(int id, String key) {
		return -1;		
	}
	
	// reserve an item
	protected boolean reserveItem(int id, int customerID, String key, String location){
		return false;	
	}
	
	// Create a new flight, or add seats to existing flight
	//  NOTE: if flightPrice <= 0 and the flight already exists, it maintains its current price
	public boolean addFlight(int id, final int flightNum, int flightSeats, int flightPrice)
		throws RemoteException, DeadlockException
	{	
		
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}

		xManager.resetTTL(id);
		writeXMan();
		xManager.getTransactionById(id).addRM(FLIGHTS);
		try{
			return rms.get(FLIGHTS).addFlight(id,flightNum,flightSeats,flightPrice);
		} catch (DeadlockException de){
			try {
				
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}

	
	public boolean deleteFlight(int id, int flightNum)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		writeXMan();
		xManager.getTransactionById(id).addRM(FLIGHTS);
		try{
			return rms.get(FLIGHTS).deleteFlight(id, flightNum);
		} catch (DeadlockException de){
			try {
				
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}



	// Create a new room location or add rooms to an existing location
	//  NOTE: if price <= 0 and the room location already exists, it maintains its current price
	public boolean addRooms(int id, String location, int count, int price)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		writeXMan();
		xManager.getTransactionById(id).addRM(HOTELS);
		try{
			return rms.get(HOTELS).addRooms(id, location, count, price);
		} catch (DeadlockException de){
			try {
			
				abort(id);

			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}

	// Delete rooms from a location
	public boolean deleteRooms(int id, String location)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		writeXMan();
		xManager.getTransactionById(id).addRM(HOTELS);
		try{
			return rms.get(HOTELS).deleteRooms(id, location);
		} catch (DeadlockException de){
			try {
				abort(id);
				
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
		
	}

	// Create a new car location or add cars to an existing location
	//  NOTE: if price <= 0 and the location already exists, it maintains its current price
	public boolean addCars(int id, String location, int count, int price)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		writeXMan();
		xManager.getTransactionById(id).addRM(CARS);
		try{
			return rms.get(CARS).addCars(id, location, count, price);
		} catch (DeadlockException de){
			try {
			
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}


	// Delete cars from a location
	public boolean deleteCars(int id, String location)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		writeXMan();
		xManager.getTransactionById(id).addRM(CARS);
		try{
			return rms.get(CARS).deleteCars(id, location);
		} catch (DeadlockException de){
			try {
			
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}



	// Returns the number of empty seats on this flight
	public int queryFlight(int id, int flightNum)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(FLIGHTS);
		try{
			return rms.get(FLIGHTS).queryFlight(id, flightNum);
		} catch (DeadlockException de){
			try {
				
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}

	// Returns the number of reservations for this flight. 
	public int queryFlightReservations(int id, int flightNum)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(FLIGHTS);
		try{
			return rms.get(FLIGHTS).queryFlightReservations(id, flightNum);
		} catch (DeadlockException de){
			try {
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}


	// Returns price of this flight
	public int queryFlightPrice(int id, int flightNum )
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(FLIGHTS);
		try{
			return rms.get(FLIGHTS).queryFlightPrice(id, flightNum);
		} catch (DeadlockException de){
			try {
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}


	// Returns the number of rooms available at a location
	public int queryRooms(int id, String location)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(HOTELS);
		try{
			return rms.get(HOTELS).queryRooms(id, location);
		} catch (DeadlockException de){
			try {
		
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}
	
	// Returns room price at this location
	public int queryRoomsPrice(int id, String location)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(HOTELS);
		try{
			return rms.get(HOTELS).queryRoomsPrice(id, location);
		} catch (DeadlockException de){
			try {
		
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}


	// Returns the number of cars available at a location
	public int queryCars(int id, String location)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(CARS);
		try{
			return rms.get(CARS).queryCars(id, location);
		} catch (DeadlockException de){
			try {
			
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}


	// Returns price of cars at this location
	public int queryCarsPrice(int id, String location)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(CARS);
		try{
			return rms.get(CARS).queryCarsPrice(id, location);
		} catch (DeadlockException de){
			try {
			
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}


	// Returns data structure containing customer reservation info. Returns null if the
	//  customer doesn't exist. Returns empty RMHashtable if customer exists but has no
	//  reservations.
	public RMHashtable getCustomerReservations(int id, int customerID)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}

		xManager.resetTTL(id);

		try{
			if(mLockMan.Lock(id, "t:" + customerID, TrxnObj.READ)) {
				Trace.info("RM::getCustomerReservations(" + id + ", " + customerID + ") called" );
				Customer cust = (Customer) readData( id, Customer.getKey(customerID) );
				if( cust == null ) {
					Trace.warn("RM::getCustomerReservations failed(" + id + ", " + customerID + ") failed--customer doesn't exist" );
					return null;
				} else {
					//RMHashtable cars_ht = rms.get(CARS).getCustomerReservations(id, customerID);
					//RMHashtable flights_ht = rms.get(FLIGHTS).getCustomerReservations(id, customerID);
					//RMHashtable hotels_ht = rms.get(HOTELS).getCustomerReservations(id, customerID);


					return cust.getReservations();
				} // if
			} else {
				System.out.println("Invalid lock request.");
				return null;		
			}
		} catch (DeadlockException de){
			try {
			
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
		
	}

	// return a bill
	public String queryCustomerInfo(int id, int customerID)
		throws RemoteException, DeadlockException
	{	
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(CARS);
		xManager.getTransactionById(id).addRM(FLIGHTS);
		xManager.getTransactionById(id).addRM(HOTELS);
		writeXMan();
		try{
			if(mLockMan.Lock(id, "t:" + customerID, TrxnObj.READ)) {
				Trace.info("RM::queryCustomerInfo(" + id + ", " + customerID + ") called" );
				Customer cust = (Customer) readData( id, Customer.getKey(customerID) );
				if( cust == null ) {
					Trace.warn("RM::queryCustomerInfo(" + id + ", " + customerID + ") failed--customer doesn't exist" );
					return "";
				} else {
					String s = "Bill for customer " + customerID + "\n";
					// Get all bills from subqueries
					s += rms.get(CARS).queryCustomerInfo(id, customerID) + rms.get(FLIGHTS).queryCustomerInfo(id, customerID) + rms.get(HOTELS).queryCustomerInfo(id, customerID);

					Trace.info("RM::queryCustomerInfo(" + id + ", " + customerID + "), bill follows..." );
					System.out.println( s );

					return s;
				} // if
			} else {
				System.out.println("Invalid lock request.");
				return null;
			} // if
		} catch (DeadlockException de){
			try {
	
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}

	public void restore_reservations() {
		Trace.info("INFO: Incorrect usage. Middleware does not restore reservations.");
	}

  // customer functions
  // new customer just returns a unique customer identifier
  public int newCustomer(int id)
		throws RemoteException, DeadlockException
		{

	  if(!xManager.isIdValid(id)){
		  throw new RemoteException("Invalid transaction id: " + id);
	  }
	  
	  xManager.resetTTL(id);
	  try{
		  Trace.info("INFO: RM::newCustomer(" + id + ") called" );
		  // Generate a globally unique ID for the new customer
		  int cid = Integer.parseInt( String.valueOf(id) +
				  String.valueOf(Calendar.getInstance().get(Calendar.MILLISECOND)) +
				  String.valueOf( Math.round( Math.random() * 100 + 1 )));
		  
		  Transaction t = xManager.getTransactionById(id);
		  Object[] state = null;
		  t.addWrite(RecordState.TYPE_RESERVATION, Customer.getKey(cid),state);
		  t.addRM(CARS);
		  t.addRM(FLIGHTS);
		  t.addRM(HOTELS);
		  writeXMan();
		  Customer cust = new Customer( cid );
		  if(mLockMan.Lock(id, "t:" + cid, TrxnObj.WRITE)) {
			  // Propagate knowledge to individual RMs
			  boolean result = rms.get(CARS).newCustomer(id, cid);
			  if(result) {
				  result = rms.get(FLIGHTS).newCustomer(id, cid);
				  if(result) {
					  result = rms.get(HOTELS).newCustomer(id, cid);
				  } else {
					  // FlightsRM failed; backtrack
					  rms.get(CARS).deleteCustomer(id, cid);
				  }
			  } else {
				  Trace.info("RM::newCustomer failed. No customer created with given id: " + id );
				  writeCustomers();
				  return -1;
			  }
			  if(result) { 
				  // Only write when all sub-writes are successful
				  writeData( id, cust.getKey(), cust );
				  Trace.info("RM::newCustomer(" + cid + ") returns ID=" + cid );
				  writeCustomers();
				  return cid;
			  } else {
				  // HotelsRM failed; backtrack all
				  rms.get(CARS).deleteCustomer(id, cid);
				  rms.get(FLIGHTS).deleteCustomer(id, cid);
				  Trace.info("RM::newCustomer failed. No customer created with given id: " + id );
				  writeCustomers();
				  return -1;
			  }
		  } else {
			  writeCustomers();
			  System.out.println("Invalid lock type.");
			  return -1;		
		  }
	  } catch (DeadlockException de){
		  try {
			  abort(id);
		  } catch (InvalidTransactionException e) {
			  throw new RemoteException("Server error");
		  }
		  throw new RemoteException("Server aborted transaction: " + id);
	  }
		}

	// Used by distributed RMs. Does not need to check lock because nothing is done.
  public boolean newCustomer(int id, int customerID )
		throws RemoteException, DeadlockException
	{
		Trace.info("INFO: RM::CustomerID must be generated. Correct usage: newCustomer, <int> id." );
		return false;
	}


	// Deletes customer from the database. 
	public synchronized boolean deleteCustomer(int id, int customerID)
			throws RemoteException, DeadlockException
			{

		//middleware
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		writeXMan();
		//Log before Image
		Transaction t = xManager.getTransactionById(id);
		Customer c = (Customer)readData(id,Customer.getKey(customerID));
		Customer q;
		try {
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(bos);
			oos.writeObject(c);
			oos.flush();
			oos.close();
			bos.close();
			byte [] byteData = bos.toByteArray();
			ByteArrayInputStream bais = new ByteArrayInputStream(byteData);
			q = (Customer) new ObjectInputStream(bais).readObject();
		} catch (IOException e) {
			e.printStackTrace();
			
			throw new RemoteException("Deepcopy error");
			
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
			throw new RemoteException("Deepcopy error");
			
		}
		Object[] state = new Object[] {new Integer(customerID),q};
		t.addWrite(RecordState.TYPE_RESERVATION, Customer.getKey(customerID),state);
		xManager.getTransactionById(id).addRM(CARS);
		xManager.getTransactionById(id).addRM(FLIGHTS);
		xManager.getTransactionById(id).addRM(HOTELS);
		writeXMan();

		try{

			if(mLockMan.Lock(id, "t:" + customerID, TrxnObj.WRITE)) {
				Trace.info("RM::deleteCustomer(" + id + ", " + customerID + ") called" );
				Customer cust = (Customer) readData( id, Customer.getKey(customerID) );
				if( cust == null ) {
					Trace.warn("RM::deleteCustomer(" + id + ", " + customerID + ") failed--customer doesn't exist" );
					writeCustomers();
					return false;
				} else {
					// Assume removeData returns success. So attempt all the sub-removes before updating.
					boolean result = rms.get(CARS).deleteCustomer(id, customerID);

					if(result) {
						result = rms.get(FLIGHTS).deleteCustomer(id, customerID);
						if(result) {
							result = rms.get(HOTELS).deleteCustomer(id, customerID);
						} else {
							// FlightsRM failed; backtrack Cars
							rms.get(CARS).restore_reservations();
							rms.get(CARS).newCustomer(id, customerID);
						}
					}  else {
						writeCustomers();
						return false;
					}

					if(result) { // All were successful.
						// Increase the reserved numbers of all reservable items which the customer reserved. 
						RMHashtable reservationHT = cust.getReservations();
						for(Enumeration e = reservationHT.keys(); e.hasMoreElements();){		
							String reservedkey = (String) (e.nextElement());
							ReservedItem reserveditem = cust.getReservedItem(reservedkey);
							Trace.info("RM::deleteCustomer(" + id + ", " + customerID + ") has reserved " + reserveditem.getKey() + " " +  reserveditem.getCount() +  " times"  );
							ReservableItem item  = (ReservableItem) readData(id, reserveditem.getKey());
							Trace.info("RM::deleteCustomer(" + id + ", " + customerID + ") has reserved " + reserveditem.getKey() + "which is reserved" +  item.getReserved() +  " times and is still available " + item.getCount() + " times"  );
							item.setReserved(item.getReserved()-reserveditem.getCount());
							item.setCount(item.getCount()+reserveditem.getCount());
						}

						// remove the customer from the storage
						removeData(id, cust.getKey());	
						Trace.info("RM::deleteCustomer(" + id + ", " + customerID + ") succeeded" );
						writeCustomers();
						return true;						

					} else {
						Trace.info("RM::deleteCustomer failed; restoring reservations. Customer not deleted.");
						// HotelsRM delete failed; backtrack Cars and Flights
						rms.get(CARS).restore_reservations();
						rms.get(CARS).newCustomer(id, customerID);
						rms.get(CARS).restore_reservations();
						rms.get(FLIGHTS).newCustomer(id, customerID);
						writeCustomers();
						return false;
					}

				}
			} else {
				System.out.println("Invalid lock type.");
				writeCustomers();
				return false;		
			}
		} catch(DeadlockException de){
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}

	// Frees flight reservation record. Flight reservation records help us make sure we
	//  don't delete a flight if one or more customers are holding reservations
	/**
	 * @deprecated
	 */
	public boolean freeFlightReservation(int id, int flightNum)
		throws RemoteException, DeadlockException
	{
		return rms.get(FLIGHTS).freeFlightReservation(id, flightNum);
	}
	
	
	// Adds car reservation to this customer. 
	public boolean reserveCar(int id, int customerID, String location)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(CARS);
		writeXMan();
		try{
			return rms.get(CARS).reserveCar(id, customerID, location);
		} catch (DeadlockException de){
			try {
		
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}

	// Adds room reservation to this customer. 
	public boolean reserveRoom(int id, int customerID, String location)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(HOTELS);
		writeXMan();
		try{
			return rms.get(HOTELS).reserveRoom(id, customerID, location);
		} catch (DeadlockException de){
			try {
		
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}
	// Adds flight reservation to this customer.  
	public boolean reserveFlight(int id, int customerID, int flightNum)
		throws RemoteException, DeadlockException
	{
		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		xManager.resetTTL(id);
		xManager.getTransactionById(id).addRM(FLIGHTS);
		writeXMan();
		try{
			return rms.get(FLIGHTS).reserveFlight(id, customerID, flightNum);
		} catch (DeadlockException de){
			try {
	
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}
	
	// Reserve an itinerary. Since calling multiple resources, ensure single access.
	public synchronized boolean itinerary(int id,int customerID,Vector flightNumbers,String location,boolean car,boolean room)
	throws RemoteException, DeadlockException {
		String failure_message = "RM:One or more of the reservations for customer: " + customerID + " could not be processed. No reservations made.";
		boolean flight_status = true;
		boolean car_status = true;
		boolean hotel_status = true;

		if(!xManager.isIdValid(id)){
			throw new RemoteException("Invalid transaction id: " + id);
		}
		
		Transaction t = xManager.getTransactionById(id);
		xManager.resetTTL(id);
		writeXMan();
		try{
		// Reserve the flights:
		for(int i = 0; i < flightNumbers.size(); i++) {
			if(flight_status) {
				// Support reversal?
				Trace.info("RM::Reserving flight " + flightNumbers.get(i));
				flight_status = rms.get(FLIGHTS).reserveFlight(id, customerID, Integer.parseInt(flightNumbers.get(i).toString()));
			} else {
				Trace.info(failure_message);
				return false;
			}
		}
		Trace.info("RM::Now will try to reserve car and room if applicable.");
		// Would not get here if flight reservation was failure. Reserve the car:
		if(car) {
			Trace.info("RM::Attempting to reserve the car.");
			car_status = reserveCar(id, customerID, location);
		}
		// Reserve the hotel:
		if(room && car && car_status) {	
			Trace.info("RM::Attempting to reserve the room.");
			hotel_status = reserveRoom(id, customerID, location);
		}
		if(flight_status && car_status && hotel_status) {
			Trace.info("RM::Successfully reserved itinerary for customer: " + customerID);
			return true;
		} else {
			Trace.info(failure_message);
			return false;
		}
		}
		catch (DeadlockException de){
			try {
				abort(id);
			} catch (InvalidTransactionException e) {
				throw new RemoteException("Server error");
			}
			throw new RemoteException("Server aborted transaction: " + id);
		}
	}


	@Override
	public int start() throws RemoteException {
		int id = xManager.startNewTransaction();
		writeXMan();
		System.out.println("Creating new transaction with id: " + id);
		return id;
	}


	@Override
	public boolean commit(int transactionId) throws RemoteException,
			TransactionAbortedException, InvalidTransactionException {
		
		if(!xManager.isIdValid(transactionId)){
			
			throw new RemoteException("Invalid transaction id: " + transactionId);
		}
		
		System.out.println("About to send vote request...");
		countDown(3);
		
		// 2PL: send vote request and find out if ready to commit.
		if(send_vote_request(transactionId)) {
			//  MW logs that it is sending the COMMIT message to all.
			logger.push(new Log(transactionId,COMMIT_COMMAND));
			writeCustomers();
			writeLog();
			
			xManager.resetTTL(transactionId);
			System.out.println("MW committing transaction id: " + transactionId);
			System.out.println("Releasing locks...");
			mLockMan.UnlockAll(transactionId);

			// Now if a RM dies then they will ask MW about the status of each transaction
			// it has on record.
			try {
				// All living ones voted YES; try to commit all. Only ask commits of alive RMs that are being used.
				if(xManager.getTransactionById(transactionId).usesCars()) {
					System.out.println("This transaction used Cars, sending commit...");
					countDown(2);
					if(rms.get(CARS).state()) { rms.get(CARS).commit(transactionId);
					}
					
				}
			} catch (Exception e){
				System.out.println("Could not contact car server for commit, he'll have to check with me");
			}
			
			try{
				if(xManager.getTransactionById(transactionId).usesFlights()) {
					System.out.println("This transaction used Flights, sending commit...");
					countDown(2);
					if(rms.get(FLIGHTS).state()) { rms.get(FLIGHTS).commit(transactionId);
					}
				}
			} catch(Exception e){
				System.out.println("Could not contact flight server for commit, he'll have to check with me");
			}
			
			try{
				if(xManager.getTransactionById(transactionId).usesHotels()) {
					System.out.println("This transaction used Hotels, sending commit...");
					countDown(2);
					if(rms.get(HOTELS).state()) { rms.get(HOTELS).commit(transactionId);
					}
				}
			} catch(Exception e){
				System.out.println("Could not contact hotel server, he'll have to check with me");
			}
			xManager.commit(transactionId);
			writeXMan();
			return true;
		}	
		else {
			// Someone voted no; send ABORT to all
			logger.push(new Log(transactionId,ABORT_COMMAND));
			writeLog();

			try {
				abort(transactionId);
			} catch(Exception e){ 
				e.printStackTrace(); // for testing: who died?
			}
			writeCustomers();
			return false;
		}
	}
	
	/**
	 * Counts down, whooo!
	 */
	private void countDown(int amount) {
		while(amount>=0){
			System.out.println(amount + "...");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			amount--;
		}
		
	}


	// Send a VOTE-REQ message to the RM's passing the transaction ID 
	// @return whether the the transaction is ok to commit
	private boolean send_vote_request(int transactionId) throws RemoteException {

		boolean carsResult = false;
		boolean hotelsResult = false;
		boolean flightsResult = false;

		try {

			// Only ask for vote for resources used by this transaction;
			// ie, if a transaction only uses cars, and flights fails, then
			// the transaction will be able to commit.

			if(xManager.getTransactionById(transactionId).usesCars()) {
				carsResult = rms.get(CARS).vote(transactionId);	
				
			} else {
				carsResult = true; // vote doesn't matter; set true.			
			}
			if(xManager.getTransactionById(transactionId).usesHotels()) {
				hotelsResult = rms.get(HOTELS).vote(transactionId);	
			} else {
				hotelsResult = true;		
			}
			if(xManager.getTransactionById(transactionId).usesFlights()) {
				flightsResult = rms.get(FLIGHTS).vote(transactionId);			
			} else {
				flightsResult = true;
			}
			System.out.println("Results received, Calculating decision...");
			countDown(3);
			xManager.getTransactionById(transactionId).setVoted();
			if(carsResult && hotelsResult && flightsResult) {
				System.out.println("All voted yes, committing");
				return true;
			}
			
			System.out.println("A no vote was received, no commit");
			return false; // Someone voted NO.

		} catch(Exception e) { // this should be a "failure to connect" type exception.
			System.out.println("Results received, Calculating decision...");
			countDown(3);
			System.out.println("Could not connect to a RMI; equals a NO vote");
			e.printStackTrace();
			return false;
		}
	} 


	@Override
	public void abort(int transactionId) throws RemoteException,
	InvalidTransactionException {

		if(!xManager.isIdValid(transactionId)){
			throw new RemoteException("Invalid transaction id: " + transactionId);
		}

		Transaction t = xManager.getTransactionById(transactionId);
		LinkedList<RecordState> stateList = t.getOperations(transactionId);
		for(int i = 0;i<stateList.size();i++){
			RecordState r = stateList.get(i);
			Object[] args = r.getValues();
			if(args==null){
				//Customer didnt exist
				System.out.println("Removing customer: " + r.getKey());
				removeData(transactionId,r.getKey());
			}
			else{
				int customerId = ((Integer)args[0]).intValue();
				Customer cust = (Customer)args[1];
				System.out.println("Restoring customer: " + customerId);
				removeData(transactionId, cust.getKey());
				writeData(transactionId,cust.getKey(),cust);
			}
		}
		System.out.println("Aborting transaction: " + transactionId);
		System.out.println("Releasing locks");
		mLockMan.UnlockAll(transactionId);
		xManager.abort(transactionId);
		//Each can be disconneted, everyone needs to get abort signal
		try{
			rms.get(CARS).abort(transactionId);
		}
		catch(ConnectException e){
			System.out.println("Car Server down, cannot abort there");
		}
		try{
			rms.get(FLIGHTS).abort(transactionId);
		}
		catch(ConnectException e){
			System.out.println("Flight Server down, cannot abort there");
		}
		try{
			rms.get(HOTELS).abort(transactionId);
		}
		catch(ConnectException e){
			System.out.println("Room Server down, cannot abort there");
		}
		t.abort();
		writeCustomers();
		writeXMan();
	}


	@Override
	public boolean shutdown() throws RemoteException {
		System.out.println("Shutting down car server...");
		boolean carShutdown = rms.get(CARS).shutdown();
		if(carShutdown){
			System.out.println("Car server shutdown successfully");
		}
		else{
			System.out.println("Error shutting down car server");
		}
		System.out.println("Shutting down flight server...");
		boolean flightShutdown = rms.get(FLIGHTS).shutdown();
		if(carShutdown){
			System.out.println("Flight server shutdown successfully");
		}
		else{
			System.out.println("Error shutting down flight server");
		}
		System.out.println("Shutting down room server...");
		boolean roomShutdown = rms.get(HOTELS).shutdown();
		if(carShutdown){
			System.out.println("Room server shutdown successfully");
		}
		else{
			System.out.println("Error shutting down room server");
		}
		
		System.out.println("Shutting down...");
		Thread t = new Thread(){
			public void run(){
				try {
					System.out.println(5);
					Thread.sleep(1000);
					System.out.println(4);
					Thread.sleep(1000);
					System.out.println(3);
					Thread.sleep(1000);
					System.out.println(2);
					Thread.sleep(1000);
					System.out.println(1);
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.exit(0);
			}
		};
		t.start();
		return carShutdown&&flightShutdown&&roomShutdown;
	}
	
	/* Everything below this line is for deliv 3 */
	
	/* The interface exposed between the DistRMI's and the Middleware for passing messages */
	public boolean crash(String which) throws RemoteException {
		if("cars".equals(which)){
			try{
				rms.get(CARS).selfDestruct();
			}catch(Exception e){
				
			}
			return true;
		}
		if("flights".equals(which)){
			//kill flights
			try{
				rms.get(FLIGHTS).selfDestruct();
			}catch(Exception e){
				
			}
			return true;
		}
		if("hotels".equals(which)){
			//kill hotels
			try{
				rms.get(HOTELS).selfDestruct();
			}
			catch(Exception e){
				//Dont want nasty stuff returned to client
			}
			return true;
		}
		if("middleware".equals(which)){
			//kill self
			Thread t = new Thread(){
				public void run(){
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					try {
						selfDestruct();
					} catch (RemoteException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			};
			t.start();
			return true;
		}
		return false;
	}

	public boolean prepare(int transactionId) throws RemoteException, TransactionAbortedException, InvalidTransactionException {
		return false;	
	}

	/* Called by the Middleware on the DistRMI's */
	public boolean vote(int transactionId) {
		return false;
	}

	/* Called by the RM on the Middleware to get the state. ie, the RM  sent a COMMIT-vote but crashed 
	* before getting the Middleware's decision.
	*/
	public boolean requestTransactionState(int transactionId) {
		Log temp;

		for(int i = 0; i < logger.size(); i++) {
			temp = (Log) logger.elementAt(i);
			if(temp.getXID() == transactionId) {
				if(temp.getResult() == COMMIT_COMMAND) {
					return true;
				} else {
					return false;
				}
			}
		}

		System.out.println("Could not find the transaction in the log record.");
		return false;
	}

    public boolean state() throws RemoteException {
		return true;
	}


	@Override
	public void selfDestruct() throws RemoteException {
		System.out.println("Killing self");
		System.exit(0);
	}
}
